import struct
from abc import abstractmethod
from copy import copy
from dataclasses import dataclass
from datetime import datetime
from io import BytesIO
from typing import Sequence, List, Union, Dict, Tuple

import numpy

from emi.core.Context import Context
from emi.trade.data import AccountData, PositionData, Direction, DailyResult, OpType, OrderData, Offset, TradeData
from emi.trade.trader import PortfolioData
from emi.util import utils
from emi.util.statistics import BoxPlot


@dataclass
class HistoryHoldData:
    """
    历史个股持仓单。（完整的持仓操作）
    """
    symbol: str
    start_time: datetime  ##开始时间
    end_time: datetime  ##结束时间
    days: int  ##持有天数
    pnl: float
    trade_data_list: List[TradeData]  ## 具体买入，卖出操作。

@dataclass
class HoldingData:

    """
    正在持有单
    """
    symbol: str
    position: PositionData   ##  仓位数据
    trade_data_list: List[TradeData]  ## 具体买入，卖出操作。

    @property
    def time(self):
        return self.trade_data_list[0].time

@dataclass
class StatisticResult:
     """
     统计回测结果
     """
     init_capital: float = 0  ## 初始资金
     capital: float = 0  ## 当前资金
     profit: float = 0  ## 总利润
     start: datetime = None  # 回测开始时间
     end: datetime = None  ## 回测结束时间
     pnl: float = 0.0  ## 总收益
     pnl_down: float = 0  ## 最大回撤
     commission: float = 0  ## 总佣金费
     commission_rate: float = 0  # 佣金费率
     days: int = 0  ## 总交易日
     days_porfit: int = 0  ## 盈利日
     days_loss: int = 0  ## 亏损日
     trade_count: int = 0  ## 产生的交易总数，包含买入、卖出
     trade_count_buy: int = 0  ##
     trade_count_sell: int = 0  ##
     capital_use_ratio: float = 0.0  ## 资金利用率
     pnl_desc: [] = None  ## [平均收益，盈利率，盈利均值，亏本率，亏本均值]

     update_time: datetime = None


     def to_bytes(self) -> bytes:
         from emi import io as BaseIO
         output = BytesIO()
         output.write(struct.pack("=dd", self.start.timestamp(), self.end.timestamp()))
         BaseIO.write_float_compress(output, [self.init_capital, self.capital, self.profit,
                                              self.pnl, self.pnl_down, self.commission, self.commission_rate,
                                              self.capital_use_ratio
                                              ], presicion=5)
         BaseIO.write_int_compress(output, [self.days, self.days_porfit, self.days_loss, self.trade_count,
                                            self.trade_count_sell, self.trade_count_buy])
         BaseIO.write_float_compress(output, self.pnl_desc)
         output.write(struct.pack("=d", self.update_time.timestamp()))
         return output.getvalue()

     @staticmethod
     def load(data: bytes):
         from emi import io as BaseIO
         offset = 0
         self = StatisticResult()
         (_start_timestamp, _end_timestamp) = struct.unpack_from("=dd", data, offset)
         offset += 16
         [self.init_capital, self.capital, self.profit,
          self.pnl, self.pnl_down, self.commission, self.commission_rate, self.capital_use_ratio
          ], offset = BaseIO.read_float_compress(data, offset)
         self.start = datetime.fromtimestamp(_start_timestamp)
         self.end = datetime.fromtimestamp(_end_timestamp)
         [self.days, self.days_porfit, self.days_loss, self.trade_count, self.trade_count_sell,
          self.trade_count_buy], offset = BaseIO.read_int_compress(data, offset)
         self.pnl_desc, offset = BaseIO.read_float_compress(data, offset)
         try:
             _update_timestamp = struct.unpack_from("=d", data, offset)
             self.update_time = datetime.fromtimestamp(_update_timestamp[0])
         except Exception:
             pass
         return self

@dataclass
class PortfolioDataStatistic:
    """
    PortfolioData 统计
    """
    portfolio_data:PortfolioData

    holding_datas:List[HoldingData] = None   ## 正在持有的单
    history_hold_datas:List[HistoryHoldData]= None   ## 历史个股完整持仓单。（完整的持仓操作）

    init_capital:float = 0 ## 初始资金
    capital:float = 0 ## 当前资金
    profit:float = 0  ## 总利润

    start:datetime = None   #开始时间
    end:datetime = None   ##结束时间

    pnl:float = 0.0  ## 总收益
    pnl_down = 0 ## 最大回撤

    commission:float = 0  ## 总佣金费
    commission_rate:float = 0  #佣金费率
    one_hand_size:int = 0  #一手数量
    days:int = 0   ## 总交易日
    days_porfit = 0  ## 盈利日
    days_loss = 0   ## 亏损日
    trade_count = 0  ## 产生的交易总数，包含买入、卖出
    trade_count_buy = 0  ##
    trade_count_sell = 0  ##
    capital_use_ratio = 0.0  ## 资金利用率
    pnl_desc:[] = None  ## [平均收益，盈利率，盈利均值，亏本率，亏本均值]
    pnl_win:[] = None   ## 盈利分布

    class Builder:

        def build_history(self, origin_datas: List[TradeData]) -> HistoryHoldData:
            """
            构建HistoryHoldData
            :origin_datas  不要修改origin_datas里面的内容，需要需要的应该拷贝一份。
            :return:
            """
            raise NotImplementedError()

    @staticmethod
    def parse(data:PortfolioData,builder:Builder = None):
        if builder is None:
            builder = DefaultStatisticBuilder()
        statstic = PortfolioDataStatistic(portfolio_data=data)

        daily_results = list(data.daily_results) if data.daily_results else []
        if data.today_daily_result:
            daily_results.append(data.today_daily_result)
        if len(daily_results) == 0:
            ## 没有回测数据
            return statstic
        statstic.history_hold_datas,statstic.holding_datas = statstic.__cal_hold_data(builder)
        statstic.init_capital = data.init_capital
        statstic.capital = daily_results[-1].balance
        statstic.profit = statstic.capital - statstic.init_capital
        statstic.start = daily_results[0].time
        statstic.end = daily_results[-1].time

        statstic.commission_rate = data.COMMISSION_RATE
        statstic.one_hand_size = data.ONE_HAND_SIZE
        statstic.days = len(daily_results)
        assert statstic.days == (daily_results[-1].day_token - daily_results[0].day_token + 1)

        _previous_balance = statstic.init_capital
        _max_balance = statstic.init_capital
        capital_use_ratios = []
        _high_blance = daily_results[-1].balance
        for daily_result in daily_results:
            _cur_balance = daily_result.balance
            _today_profit = _cur_balance - _previous_balance
            _pnl = 100 * (_cur_balance - statstic.init_capital) / statstic.init_capital
            if _today_profit > 0.01:
                statstic.days_porfit += 1
            elif _today_profit < -0.01:
                statstic.days_loss += 1
            statstic.trade_count += len(daily_result.tradeDatas)
            capital_use_ratios.append(daily_result.balance_hold / daily_result.balance)  ## 当天资金利用率
            ## 计算最大回撤
            if _cur_balance > _max_balance:
                _max_balance = _cur_balance
            else:
                _cur_pnl_callback =  100 * (_cur_balance - _max_balance) / _max_balance
                if _cur_pnl_callback < statstic.pnl_down:
                    statstic.pnl_down = _cur_pnl_callback
            _previous_balance = _cur_balance
            for trade_data in daily_result.tradeDatas:
                statstic.commission += trade_data.commission
                ##只考虑做多的情况
                assert trade_data.direction == Direction.LONG
        statstic.capital_use_ratio = numpy.array(capital_use_ratios).mean()

        win_pnls = []
        loss_pnls = []
        all_pnls = []
        avg_pnl = 0.0  # 平均收益
        win_pnl = 0.0  # 盈利均值
        loss_pnl = 0.0  # 亏本均值
        win_rate = 0.0  # 盈利率
        loss_rate = 0.0  # 亏本率

        _total_all_pnl = 0
        _total_win_pnl = 0
        _total_loss_pnl = 0

        for hold_data in statstic.history_hold_datas:
            pnl = hold_data.pnl
            _total_all_pnl+=pnl
            all_pnls.append(pnl)
            if pnl > 0.1:
                _total_win_pnl+=pnl
                win_pnls.append(pnl)
            elif pnl < -0.1:
                _total_loss_pnl+=pnl
                loss_pnls.append(pnl)
        if all_pnls:
            avg_pnl = _total_all_pnl / len(all_pnls)
        if win_pnls:
            win_pnl = _total_win_pnl / len(win_pnls)
            win_rate = len(win_pnls) / len(all_pnls)
        if loss_pnls:
            loss_pnl = _total_loss_pnl / len(loss_pnls)
            loss_rate = len(loss_pnls) / len(all_pnls)

        statstic.pnl_win = win_pnls
        statstic.pnl_desc = [avg_pnl,win_rate,win_pnl,loss_rate,loss_pnl]## [平均收益，盈利率，盈利均值，亏本率，亏本均值]
        statstic.pnl = statstic.profit / statstic.init_capital * 100
        return statstic

    def get_desc(self):
        line = "-" * 30
        INIT_CAPITAL = self.init_capital
        line += f"\n时间段：{self.start} -- {self.end}"
        line += f"\n总交易日：{self.days} , 盈利日占比:{100 * self.days_porfit / self.days:.2f}%   ,亏损日占比:{100 * self.days_loss / self.days:.2f}%"
        line += f"\n初始资金：{INIT_CAPITAL:.2f}, " \
                f"单边佣金费率:{self.commission_rate * 1000:.2f}%%," \
                f"一手数量: {self.one_hand_size}"
        line += f"\n总收益率：{self.pnl:.2f}%,  盈亏金额:{self.profit:.2f}"
        line += f"\n总佣金费：{self.commission:.1f},   占初始资金: {100 * self.commission / INIT_CAPITAL:.1f}%"
        line += f"\n最大回撤：{self.pnl_down:.2f}%"
        line += f"\n平均收益：{self.pnl_desc[0]:.2f}% , 盈利率:{100 * self.pnl_desc[1]:.1f}%, 盈利均值:{self.pnl_desc[2]:.1f}%, 亏本率:{100 * self.pnl_desc[3]:.1f}%,亏本均值:{self.pnl_desc[4]:.1f}%"

        line += f"\n资金利用率：{self.capital_use_ratio * 100:.2f}%"

        history_data_list = self.history_hold_datas
        history_data_size = len(history_data_list)
        line += f"\n\n 历史持仓统计:({history_data_size}): \n\n"
        if history_data_size > 0:
            pnl_list = []
            day_list = []
            for hold_data in history_data_list:
                pnl_list.append(hold_data.pnl)
                day_list.append(hold_data.days)
            desc_text = BoxPlot.get_str([
                BoxPlot.of_values(pnl_list, "pnl"),
                BoxPlot.of_values(day_list, "days"),
                BoxPlot.of_values(self.pnl_win,"盈利值分布")
            ])
            line += desc_text
            line += "\n\n"



        return line



    def __cal_hold_data(self,builder:Builder) -> Tuple[List[HistoryHoldData],List[HoldingData]]:
        daily_results = self.portfolio_data.daily_results
        if not daily_results:
            return ([],[])

        @dataclass
        class BuildSession:
            symbol:str
            trade_list: List[TradeData]
            def reset(self):
                self.trade_list = []

        session_map: Dict[str, BuildSession] = {}
        history_hold_list = []
        for daily_result in daily_results:
            for trade_data in daily_result.tradeDatas:
                ##只考虑做多的情况
                assert trade_data.direction == Direction.LONG
                if trade_data.offset == Offset.OPEN:
                    # 买入
                    builder_session = session_map.get(trade_data.symbol)
                    if builder_session is None:
                        builder_session = BuildSession(symbol=trade_data.symbol,trade_list=[])
                        session_map[trade_data.symbol] = builder_session
                    builder_session.trade_list.append(trade_data)
                elif trade_data.offset == Offset.CLOSE:
                    # 卖出
                    builder_session = session_map.get(trade_data.symbol)
                    builder_session.trade_list.append(trade_data)
                    trade_list = [ copy(trade)  for trade in builder_session.trade_list]
                    pos_data = builder.build_history(trade_list)
                    if pos_data:
                        builder_session.reset()
                        del session_map[trade_data.symbol]
                        history_hold_list.append(pos_data)

        holding_list = []
        for session in session_map.values():
            if session.trade_list:
                holding_list.append(HoldingData(
                    symbol=session.symbol,
                    position=self.portfolio_data.get_position(session.symbol,direction=Direction.LONG),
                    trade_data_list=session.trade_list
                ))

        assert len(holding_list) == len(self.portfolio_data.get_all_positions(Direction.LONG))
        return (history_hold_list,holding_list)


    def to_result(self)->StatisticResult:
        statistic_data = self
        bt_result = StatisticResult(
            init_capital=statistic_data.init_capital,
            capital=statistic_data.capital,
            profit=statistic_data.profit,
            start=statistic_data.start,
            end=statistic_data.end,
            pnl=statistic_data.pnl,
            pnl_down=statistic_data.pnl_down,
            commission=statistic_data.commission,
            commission_rate=statistic_data.commission_rate,
            days=statistic_data.days,
            days_porfit=statistic_data.days_porfit,
            days_loss=statistic_data.days_loss,
            trade_count=statistic_data.trade_count,
            trade_count_buy=statistic_data.trade_count_buy,
            trade_count_sell=statistic_data.trade_count_sell,
            capital_use_ratio=statistic_data.capital_use_ratio,
            pnl_desc=statistic_data.pnl_desc,
        )
        bt_result.update_time = datetime.now()
        return bt_result


class DefaultStatisticBuilder(PortfolioDataStatistic.Builder):
    """
    统计构建器
    """

    def build_history(self, origin_datas: List[TradeData]) -> HistoryHoldData:
        """
        构建HistoryHoldData
        :origin_datas  不要修改origin_datas里面的内容，需要需要的应该拷贝一份。
        :return:
        """
        trade_data_list = [copy(trade) for trade in origin_datas]  ## 主要要拷贝一份
        if not trade_data_list:
            return None
        base_pnl_vol = None  ## pnl 基础仓位。
        for trade in trade_data_list:
            if trade.offset == Offset.OPEN:
                base_pnl_vol = trade.volume  ## 第一个买入的仓位作为pnl的基础仓位。
                break
        buy_order_list: List[TradeData] = []
        sell_order_list: List[TradeData] = []
        for order in trade_data_list:
            if order.offset == Offset.OPEN:
                buy_order_list.append(order)
            else:
                assert order.offset == Offset.CLOSE
                sell_order_list.append(order)
        ### 交割单
        cross_data_list = []  # 买入价，卖出价，成交量
        buy_order_list.reverse()
        sell_order_list.reverse()
        try:
            while True:
                sell_order = sell_order_list.pop()
                while True:
                    buy_order = buy_order_list.pop()
                    assert buy_order.volume > 0 and sell_order.volume > 0
                    if buy_order.volume >= sell_order.volume:
                        cross_data_list.append([buy_order.price, sell_order.price, sell_order.volume])
                        buy_order.volume -= sell_order.volume
                        sell_order.volume = 0
                        if buy_order.volume > 0:
                            buy_order_list.append(buy_order)
                    else:
                        cross_data_list.append([buy_order.price, sell_order.price, buy_order.volume])
                        sell_order.volume -= buy_order.volume
                        buy_order.volume = 0
                    if sell_order.volume <= 0:
                        break
                if len(sell_order_list) == 0 and len(buy_order_list) == 0:
                    break
        except IndexError:
            ## 交割没有闭环
            return None

        pnl = 0.0
        for cross_data in cross_data_list:
            buy_price = cross_data[0]
            sell_price = cross_data[1]
            position = cross_data[2]
            pnl += 100 * (sell_price - buy_price) / buy_price * position / base_pnl_vol

        return HistoryHoldData(symbol=origin_datas[0].symbol,
                               start_time=origin_datas[0].time,
                               end_time=origin_datas[-1].time,
                               pnl=pnl,
                               days=origin_datas[-1].day_token - trade_data_list[
                                   0].day_token,
                               trade_data_list=origin_datas
                               )
